<?php
require_once FORAGE_APP_PATH . '/engine/solr/SolrQuery.php';
require_once FORAGE_APP_PATH . '/engine/solr/SolrResponse.php';

/**
 * This class is responsible for interfacing with the Solr
 * enterprise search server.
 *
 * @author Rob Young <rob@roryoung.co.uk>
 * @package forage.engine.solr
 */
class ForageSolrEngine extends ForageEngine implements ForageEngineModifiable, ForageEngineSearchable
{
  /**
   * Target dsn
   * 
   * @var string
   */
  private $target;

  /**
   * Configuration options
   *
   * @var array
   */
  private $config;

  /**
   * Initialize this object.
   *
   * @param string $dsn
   * @param array $config
   */
  public function initialize($dsn, $config)
  {
    if (!preg_match('#([a-z0-9\-\._]+)(:\d+)?(/.*)?#i', $dsn, $matches)) {
      throw new InvalidArgumentException("Invalid Solr path provided [" . $dsn . "]");
    }
    $this->target = $dsn . (empty($matches[3]) ? '/solr' : '');
    $this->config = $config;
  }

  /**
   * Return a specific solr query object.
   *
   * @return ForageSolrQuery
   */
  public function getQuery()
  {
    return new ForageSolrQuery();
  }
  
  /**
   * Perform a search on the Solr index.
   *
   * @param ForageQuery $query
   * @return ForageResult
   */
  public function search($query)
  {
    if (!($query instanceof ForageSolrQuery)) {
      throw new InvalidArgumentException("Expecting a ForageSolrQuery");
    }
    $q = $this->buildQueryString($query);
    $string = $this->sendSearch('http://' . $this->target . '/select/?' . $q);
    $xml = new SimpleXMLElement($string);
    $response = $this->getResponse();
    $response->setRawResponse($string);
    $responseElement = $xml->xpath("//result[@name='response']");
    $response->setTotal((string)$responseElement[0]['numFound']);
    $this->addFacets($response, $xml, $query);
    foreach ($xml->xpath('//result/doc') as $docXml) {
      $doc = new ForageDocument();
      foreach ($docXml->xpath('str') as $strXml) {
        $doc[] = new ForageField((string)$strXml['name'], (string)$strXml);
      }
      $score = $docXml->xpath("float[@name='score']");
      if (!empty($score[0])) {
        $doc->setProperty('score', (float)$score[0]);
      }
      $response[] = $doc;
    }
    return $response; 
  }

  /**
   * Add a document to the Solr index.
   *
   * @param ForageDocument $document
   */
  public function add(ForageDocument $document)
  {
    $xml = $this->createAddDocument($document);
    $this->sendUpdate($xml);
  }
  
  /**
   * Remove a document from the Solr index
   *
   * @param ForageDocument $document
   */
  public function remove(ForageDocument $document)
  {
    $xml = $this->createRemoveDocument($document);
    $this->sendUpdate($xml);
  }

  /**
   * Flush changes to the index.
   */
  public function flush()
  {
    $xml = $this->createCommitDocument();
    $this->sendUpdate($xml);
  }

  /**
   * Optimize the index.
   */
  public function optimize()
  {

  }

  /**
   * Return a specific solr response object.
   *
   * @return ForageSolrResponse
   */
  protected function getResponse()
  {
    return new ForageSolrResponse();
  }

  /**
   * Convert a ForageSolrQuery object into a Solr query string
   *
   * @param ForageSolrQuery $query
   * @return string
   */
  protected function buildQueryString(ForageSolrQuery $query)
  {
    $fields = array();
    $this->addQueryFields($query, $fields);
    $this->addPaginationFields($query, $fields);
    $this->addResponseFields($query, $fields);
    $this->addSortFields($query, $fields);
    $this->addFacetFields($query, $fields);
    $this->addGeoFields($query, $fields);
    $encoded = array();
    foreach ($fields as $name => $value) {
      if (!is_array($value)) {
        $encoded[] = $this->encode($name, $value);
      } else {
        foreach ($value as $v) {
          $encoded[] = $this->encode($name, $v);
        }
      }
    }

    return implode('&', $encoded);
  }

  protected function encode($name, $value)
  {
    return $name . '=' . urlencode($value);
  }

  protected function addQueryFields($query, &$fields)
  {
    $filters = $query->getFacetFilters();
    if (!$filters) {
      $qs = $query->query_string;
    } else {
      $fs = '';
      foreach ($filters as $name => $values) {
        foreach ($values as $value) {
          $fs .= ' +' . $name . ':"' . addslashes($value) . '"';
        }
      }
      if ($query->query_string) {
        $qs = '+(' . $query->query_string . ')' . $fs;
      } else {
        $qs = $fs;
      }
    }
    if (!$qs) {
      $qs = '*:*';
    }
    $fields['q'] = $qs;
  }

  protected function addPaginationFields($query, &$fields)
  {
    $fields['start'] = $query->start_index;
    $fields['rows']  = $query->per_page;
  }

  protected function addResponseFields($query, &$fields)
  {
    $fields['fl'] = '*,score';
  }

  protected function addSortFields($query, &$fields)
  {
    $sort = '';
    foreach ($query->sort_fields as $field => $direction) {
      $sort .= ($sort?',':'') . $field . ' ' . ($direction==SORT_ASC?'asc':'desc');
    }
    if ($sort) {
      $fields['sort'] = $sort;
    }
  }

  protected function addFacetFields($query, &$fields)
  {
    $facets = $query->facet_fields;
    if ($facets) {
      $fields['facet']          = 'true';
      $fields['facet.mincount'] = '1';
      $fields['facet.field']    = array();
      foreach ($facets as $field) {
        $fields['facet.field'][] = $field;
      }
    }
  }

  protected function addGeoFields($query, &$fields)
  {
    if ($query->hasBehaviour('ForageGeoQueryBehaviour')) {
      $lat  = $query->latitude;
      $long = $query->longitude;
      if ($lat && $long) {
        $fields['qt']   = 'geo';
        $fields['lat']  = $lat;
        $fields['long'] = $long;
        $radius = $query->radius;
        if ($radius) {
          $fields['radius'] = $radius;
        }
      }
    }
  }

  /**
   * Add facets from a raw Solr response into a ForageSolrResponse
   * adding facet filters from the ForageSolrQuery
   *
   * @param ForageSolrResponse $response
   * @param string $xml
   * @param ForageSolrQuery $query
   */
  protected function addFacets($response, $xml, $query)
  {
    $fields = $xml->xpath('//lst[@name="facet_fields"]/lst');
    if ($fields) {
      foreach ($fields as $field) {
        $name    = (string)$field['name'];
        $filters = $query->getFacetFilters($name);
        foreach ($field->xpath('int') as $value) {
          $is_filter = $filters && in_array((string)$value['name'], $filters);
          $response->add($name, (string)$value['name'], (string)$value, $is_filter);
        }
      }
    }
  }

  /**
   * Create and 'add' xml from a ForageDocument object.
   *
   * @param ForageDocument $document
   * @return string
   */
  public function createAddDocument(ForageDocument $document)
  {
    $domDocument = new DOMDocument('1.0', 'UTF-8');
    $addElement  = $domDocument->createElement('add');
    $domDocument->appendChild($addElement);
    $docElement = $domDocument->createElement('doc');
    $addElement->appendChild($docElement);
    if (isset($document->properties['boost'])) {
      $docElement->setAttribute('boost', $document->properties['boost']);
    }
    // add the fields
    foreach ($document->fields as $field) {
      $fieldElement = $domDocument->createElement('field');
      $fieldElement->appendChild($domDocument->createTextNode($field->value));
      $fieldElement->setAttribute('name', $field->name);
      if (isset($field->properties['boost'])) {
        $fieldElement->setAttribute('boost', $field->properties['boost']);
      }
      $docElement->appendChild($fieldElement);
    }

    return $domDocument->saveXML();
  }

  /**
   * Create a 'commit' xml document.
   *
   * @return string
   */
  public function createCommitDocument()
  {
    $domDocument   = new DOMDocument('1.0', 'UTF-8');
    $commitElement = $domDocument->createElement('commit');
    $domDocument->appendChild($commitElement);
    return $domDocument->saveXML();
  }

  /**
   * Create a 'delete' xml document
   *
   * @return string
   */
  public function createRemoveDocument(ForageDocument $document)
  {
    $uid = $document->uid;
    if ($uid === null) {
      throw new ForageException("Document must have a uid");
    }
    $domDocument   = new DOMDocument('1.0', 'UTF-8');
    $removeElement = $domDocument->createElement('delete');
    $domDocument->appendChild($removeElement);
    $idElement     = $domDocument->createElement('id');
    $removeElement->appendChild($idElement);
    $idElement->appendChild($domDocument->createTextNode($uid));
    return $domDocument->saveXML();
  }

  /**
   * Send an xml document to the update url.
   *
   * @param string $xml
   */
  public function sendUpdate($xml)
  {
    $curl = curl_init($this->target . '/update/');
    curl_setopt_array($curl,
      array(
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => array('Content-Type: application/xml'),
        CURLOPT_POSTFIELDS     => $xml,
        CURLOPT_HEADER         => false,
        CURLOPT_RETURNTRANSFER => true
      )
    );
    $response = curl_exec($curl);

    if (curl_errno($curl)) {
      throw new ForageSolrEngineException('Curl error: ' . curl_error($curl) . "\nOn xml: " . $xml);
    }

    if (strpos($response, '<int name="status">0</int>') === false) {
      throw new ForageSolrEngineException("Solr update failed: " . $response . "\n\n" . $xml);
    }
  }

  /**
   * Retrieve the search result xml document for a search from Solr
   *
   * @param string $url
   * @return string
   */
  protected function sendSearch($url)
  {
    return file_get_contents($url);
  }
}

/**
 * Exception for solr classes
 * 
 * @package forage.engine.solr
 */
class ForageSolrEngineException extends ForageEngineException {}
